<?php

function _CUTF8DecodeCharacterInBufferAtOffset(&$buffer, $offset) {
	$bytea = @ord($buffer[$offset]);
	
	if ($bytea < 0x80)
		return $bytea; // ASCII compatibility
	
	if (is_null($bytea) || ($bytea >= 0x80 && $bytea <= 0xC2)) {
		trigger_error("Character at offset $offset (with byte $bytea) cannot be decoded", E_USER_ERROR);
		return null;
	}
	
	$byteb = @ord($buffer[$offset + 1]);
	if (is_null($byteb) || (($byteb & 0xC0 /* 11... */) != 0x80 /* 10... */)) {
		trigger_error("Character at offset $offset (with bytes $bytea, $byteb) cannot be decoded", E_USER_ERROR);
		return null;
	}
	
	if ($bytea >= 0xC2 && $bytea <= 0xDF) {
		// 00000000 00000yyy yyzzzzzz
		// encoded as a: 110yyyyy(C2-DF) b: 10zzzzzz
		return
			($bytea & 0x1F /* 00011111 */) << 6 |
			($byteb & 0x7F) /* 01111111 */;
	}
	
	$bytec = ord($buffer[$offset + 2]);
	if (is_null($bytec) || (($bytec & 0xC0 /* 11... */) != 0x80 /* 10... */)) {
		trigger_error("Character at offset $offset (with bytes $bytea, $byteb, $bytec) cannot be decoded", E_USER_ERROR);
		return null;
	}
		
	if ($bytea >= 0xE0 && $bytea <= 0xEF) {
		// 00000000 xxxxyyyy yyzzzzzz
		// encoded as a: 1110xxxx(E0-EF) b: 10yyyyyy c: 10zzzzzz
		return
			($bytea & 0xF /* 00001111 */) << 12 |
			($byteb & 0x7F) << 6 |
			($bytec & 0x7F);
	}
	
	$byted = ord($buffer[$offset + 3]);
	if (is_null($byted) || (($byted & 0xC0 /* 11... */) != 0x80 /* 10... */))
		return null; // error!

	if ($bytea >= 0xF0 && $bytea <= 0xF4) {
		// 000wwwxx xxxxyyyy yyzzzzzz
		// encoded as a: 11110www(F0-F4) b: 10xxxxxx c: 10yyyyyy d: 10zzzzzz
		return
			($bytea & 0x7 /* 00000111 */) << 18 |
			($byteb & 0x7F) << 12 |
			($bytec & 0x7F) << 6 |
			($byted & 0x7F);
	}
	
	trigger_error("Character at offset $offset (with bytes $bytea, $byteb, $bytec, $byted) cannot be decoded", E_USER_ERROR);
	// all other forms banned by RFC 3629
	// see http://tools.ietf.org/html/rfc3629
}

function _CUTF8EncodeCharacter($c) {
	if ($c < 0) return null;
	
	if ($c <= 0x7F)
		return chr($c);
	
	if ($c <= 0x7FF) {
		$a = ($c & 0x7C0) >> 6;
		$b = ($c & 0x3F);
		
		$a = $a | 0xC0;
		$b = $b | 0x80;
		return chr($a) . chr($b);
	}
	
	if ($c <= 0xFFFF) {
		$a = ($c & 0xF000) >> 12;
		$b = ($c & 0xFC0) >> 6;
		$d = ($c & 0x3F);
		
		$a = $a | 0xE0;
		$b = $b | 0x80;
		$d = $d | 0x80;
		return chr($a) . chr($b) . chr($d);
	}
	
	if ($c <= 0x10FFFF) {
		$a = ($c & 0x1C0000) >> 18;
		$b = ($c & 0x3F000) >> 12;
		$d = ($c & 0xFC0) >> 6;
		$e = ($c & 0x3F);
		
		$a = $a | 0xF0;
		$b = $b | 0x80;
		$d = $d | 0x80;
		$e = $e | 0x80;
		return chr($a) . chr($b) . chr($d) . chr($e);
	}
	
	return null;
}

class CUTF_8String extends CString {
	var $Length;
	var $Buffer;
	var $BufLen;
	
	function Init($buf) {
		$this->Buffer = $buf;
		$this->BufLen = strlen($buf);
	}
	
	function MakeNewFromBuffer($buf) {
		return new CUTF_8String($buf);
	}
	
	function &Characters($from, $len) {
		$x = &new CUTF_8CharIterator($this, $from, $len);
		return $x;
	}
	
	function Length() {
		$this->_CalculateLengthIfNeeded();
		return $this->Length;
	}
	
	function _CalculateLengthIfNeeded() {
		if (!is_null($this->Length))
			return;
			
		$offset = 0; $c = 0;
		while ($offset < $this->BufLen) {
			$c++;
			$x = ord($this->Buffer{$offset});			
			$offset += $this->_NextSkip($x);
		}
		
		$this->Length = $c;
	}
	
	function _NextSkip($x) {	
		if ($x < 0x80)
			$offset = 1;
		else if ($x >= 0xC2 && $x <= 0xDF)
			$offset = 2;
		else if ($x >= 0xE0 && $x <= 0xEF)
			$offset = 3;
		else if ($x >= 0xF0 && $x <= 0xF4)
			$offset = 4;
		else
			trigger_error("CUTF_8String::_NextSkip(): Invalid first byte caught while looking for a new offset: $x", E_USER_ERROR);
			
		return $offset;
	}
	
	function _NextSkipFromOffset($offset) {
		return $this->_NextSkip(ord($this->Buffer[$offset]));
	}
	
	function _OffsetForCharacter($charNo) {
		$offset = 0; $c = 0;
		while ($c < $charNo) {
			$c++;
			$x = ord($this->Buffer[$offset]);
			$offset += $this->_NextSkip($x);
		}
		
		return $offset;
	}
		
	function _CodePointForOffset($i) {
		return _CUTF8DecodeCharacterInBufferAtOffset($this->Buffer, $i);
	}
	
	function FastBufferOfEncoding($enc) {
		if ($enc == kUTF8)
			return $this->Buffer;
		return null;
	}
}

class CUTF_8CharIterator extends CCharIterator {
	var $Str;
	var $Offset;
	var $CharsLeft;
	
	function Init(&$str, $from, $len) {
		$this->Str = &$str;
		$this->Offset = $str->_OffsetForCharacter($from);
		$this->CharsLeft = min($len, $str->Length());
	}
	
	function NextChar() {		
		if ($this->CharsLeft <= 0) return;
		$cp = $this->Str->_CodePointForOffset($this->Offset);
		$skip = $this->Str->_NextSkipFromOffset($this->Offset);
		$this->Offset += $skip;
		$this->CharsLeft--;
		// echo "cp = "; var_dump($cp); // D
		// echo "skip = "; var_dump($skip); // D
				
		return $cp;
	}
}

class CUTF_8Encoder extends CBufferEncoder {
	function AppendString($str) {
		if ($this->FastAppendIfEncoding($str, kASCII) || 
			$this->FastAppendIfEncoding($str, kUTF8))
			return;
		
		$buf = $str->FastBufferOfEncoding(kUTF8);
		if (!is_null($buf) && $this->FastAppendBuffer(utf8_decode($buf)))
			return;
		
		parent::AppendString($str);
	}
	
	function BytesForChar($x) {
		return _CUTF8EncodeCharacter($x);
	}
	
	function ReplacementBytes() {
		return "\xEF\xBF\xBD"; // U+FFFD REPLACEMENT CHARACTER
	}
}

?>